# Copyright (C) 2017-2018 Marco Barisione
#
# Released under the terms of the GNU LGPL license version 2.1 or later.

from __future__ import absolute_import, division, print_function

import json
import os
import textwrap

from . import (
    locations,
    pathutils,
    )

from .defprops import (
    DefinitionProperties,
    )

from .log import verbose


class Emitter(object):
    '''
    Generate the content of a `Dockerfile` based on a `DefinitionProperties` instance.
    '''

    def __init__(self, props, dst_dir):
        '''
        Initialize an `Emitter` instance.

        props:
            The `DefinitionProperties` instance contianing information on what to put in the
            `Dockerfile` file and which other files are needed.
        dst_dir:
            The directory where to put the `Dockerfile` file and related files.
        '''
        self._props = props
        self._dst_dir = dst_dir

        self._lines = []

        self._copyable_files_dir = os.path.join(self._dst_dir, 'files')
        pathutils.makedirs(self._copyable_files_dir)

    def _make_file_copyable(self, host_file_path):
        '''
        Allow the file at path `host_file_path` (on the host) to be copied to the image by
        creating a hard link to it from the directory sent to the Docker daemon.

        Return value:
            The path of the hard link, relative to the destination directory.
        '''
        # FIXME: what if two files have the same basenames?
        link_path = os.path.join(self._copyable_files_dir, os.path.basename(host_file_path))
        pathutils.hard_link_or_copy(host_file_path, link_path)

        assert link_path.startswith(self._dst_dir)
        return os.path.relpath(link_path, start=self._dst_dir)

    def _emit(self, text=''):
        if not text.endswith('\n'):
            text += '\n'
        if text.startswith('\n') and len(text) > 1:
            text = text[1:]
        self._lines.append(textwrap.dedent(text))

    def _emit_install(self, *what):
        if not what:
            return

        what_string = ' '.join(what)

        # We need to update the package lists every time due to Docker's caching of
        # intermediate images as a previous layer could have been installed with a now
        # stale package list.
        if self._props.deb_based:
            self._emit(
                r'''
                # Installing %(what)s.
                RUN \
                    export DEBIAN_FRONTEND=noninteractive && \
                    apt-get update -qqy && \
                    apt-get install -qqy -o=Dpkg::Use-Pty=0 \
                        --no-install-recommends \
                        %(what)s
                ''' %
                dict(what=what_string))
        elif self._props.rpm_based:
            self._emit(
                r'''
                # Installing %(what)s.
                RUN \
                    yum install -y \
                        %(what)s
                ''' %
                dict(what=what_string))
        else:
            assert False, 'Not Debian nor RPM-based but supported?'

    def _emit_run(self, *args):
        self._emit(
            r'''
            RUN \
                %(json_cmd)s
            ''' %
            dict(
                json_cmd=json.dumps(args),
                ))

    def _emit_run_for_time(self, when):
        for args in self._props.commands_to_run(when):
            self._emit_run(*args)

    def _emit_intro(self):
        self._emit('# Generated by Karton.')
        self._emit()

        self._emit('FROM %s' % self._props.docker_distro_full_name)
        self._emit()

        if self._props.maintainer:
            self._emit('MAINTAINER %s' % self._props.maintainer)
            self._emit()

    def _emit_addittional_archs(self):
        if self._props.additional_archs:
            archs_run = ['RUN \\']
            for i, arch in enumerate(self._props.additional_archs):
                if i == len(self._props.additional_archs) - 1:
                    cont = ''
                else:
                    cont = ' && \\'
                archs_run.append('    dpkg --add-architecture %s%s' % (arch, cont))
            archs_run.append('')
            archs_run.append('')
            self._emit('\n'.join(archs_run))

    def _emit_system_packages(self):
        if self._props.deb_based:
            self._emit_install('apt-utils')
            self._emit_install('locales')

        if self._props.sudo != DefinitionProperties.SUDO_NO:
            self._emit_install('sudo')

        self._emit_install('python')

    def _emit_install_clean(self):
        if self._props.deb_based:
            self._emit_run('apt-get', 'clean', '-qq')
        elif self._props.rpm_based:
            self._emit_run('yum', 'clean', 'all')

    def _emit_sudo(self):
        if self._props.sudo != DefinitionProperties.SUDO_NO:
            sudoers_path = os.path.join(self._dst_dir, 'sudoers')

            with open(sudoers_path, 'w') as sudoers_file:
                if self._props.sudo == DefinitionProperties.SUDO_PASSWORDLESS:
                    nopasswd = 'NOPASSWD: '
                else:
                    nopasswd = ''

                sudoers_file.write(textwrap.dedent(
                    '''\
                    root ALL=(ALL) ALL
                    %(username)s ALL=(ALL) %(nopasswd)sALL
                    Defaults    env_reset
                    Defaults    secure_path="%(paths)s"
                    ''' %
                    dict(
                        username=self._props.username,
                        nopasswd=nopasswd,
                        paths='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
                        )
                    ))

            self._emit(
                r'''
                COPY sudoers /etc/sudoers
                RUN chmod 440 /etc/sudoers
                ''')

    def _emit_container_code(self):
        container_code_path = os.path.join(locations.root_code_dir(), 'container-code')
        for container_script in ('session_runner.py', 'command_runner.py'):
            path = os.path.join(container_code_path, container_script)
            copyable_path = self._make_file_copyable(path)
            self._emit(
                r'''
                ADD %(copyable_path)s /karton/%(container_script)s
                RUN chmod +x /karton/%(container_script)s
                '''
                % dict(
                    copyable_path=copyable_path,
                    container_script=container_script,
                    ))

    def _emit_user_creation(self):
        self._emit(
            r'''
            RUN \
                mkdir -p $(dirname %(user_home)s) && \
                useradd -m -s /bin/bash --home-dir %(user_home)s --uid %(uid)s %(username)s && \
                chown %(username)s %(user_home)s
            ENV USER %(username)s
            USER %(username)s
            '''
            % dict(
                username=self._props.username,
                uid=self._props.uid,
                user_home=self._props.user_home,
                ))

    def _emit_copy_files(self):
        for i, (src_path, dest_path) in enumerate(self._props.copied):
            # Docker can only copy resources from inside its context, so we
            # first need to copy the file or directory.
            context_src_path = '/res-%d-%s' % (i, os.path.basename(src_path))

            pathutils.copy_path(src_path,
                                self._dst_dir + context_src_path)

            self._emit(
                r'''
                COPY %(src)s %(dest)s
                '''
                % dict(
                    src=context_src_path,
                    dest=dest_path,
                    ))

    def generate_content(self):
        '''
        Generate the `Dockerfile` content.
        '''
        self._emit_intro()
        self._emit_run_for_time(DefinitionProperties.RUN_AT_BUILD_START)
        self._emit_addittional_archs()
        self._emit_system_packages()
        self._emit_run_for_time(DefinitionProperties.RUN_AT_BUILD_BEFORE_USER_PKGS)
        self._emit_install(*self._props.packages)
        self._emit_install_clean()
        self._emit_sudo()
        self._emit_container_code()
        self._emit_user_creation()
        # After this point everything will be run as the normal user, not root!
        self._emit_copy_files()
        self._emit_run_for_time(DefinitionProperties.RUN_AT_BUILD_END)

        content = ''.join(self._lines).strip() + '\n'
        verbose('The Dockerfile is:\n========\n%s========' % content)

        self._lines = []

        return content
